<?php 

declare(strict_types=1);

namespace WanZ\App\Utils;

use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\DbConnection\Db;
use Hyperf\Context\Context;
use Hyperf\Redis\RedisFactory;
use Notice\Notice\NoticeFactory;
use MoChat\Framework\Constants\ErrorCode;
use MoChat\Framework\Exception\CommonException;

/**
 * Class Snowflake
 * @package WanZ\App\Utils
 * 
 */
class Snowflake
{
    //use BaseCache;

    private const WEB_HOOK_URL = 'https://oapi.dingtalk.com/robot/send?access_token=3d141b998d0439b72bb3b4fa8dbf3892b0e0ba374e612637de78ee3b1a7695ef';
    private const USER_ID_KEY = 'assignUserIds';

    //起始时间戳，毫秒(从2020-01-01开始计算)
    private const EPOCH = 1609430400000;

    //序号部分12位
    private const SEQUENCE_BITS = 12;

    //序号最大值，相当于2^12 -1
    private const SEQUENCE_MAX = -1 ^ (-1 << self::SEQUENCE_BITS);

    //节点部分10位
    private const WORKER_BITS = 10;

    //节点最大数值，相当于2^10 -1
    private const WORKER_MAX = -1 ^ (-1 << self::WORKER_BITS);

    //时间戳向左偏移量(22位)
    private const TIME_SHIFT = self::WORKER_BITS + self::SEQUENCE_BITS;

    //节点向左偏移量(12位)
    private const WORKER_SHIFT = self::SEQUENCE_BITS;

    protected $timestamp;   // 上次ID生成时间戳
    protected $workerId;    // 节点ID
    protected $sequence;    // 序号

    /**
     * @Inject
     * @var StdoutLoggerInterface
     */
    private $logger;

    /**
     * 初始化工作节点
     *
     * Snowflake constructor.
     * @param int $workerId  工作节点；默认是一个
     * @throws Exception
     */
    public function __construct(int $workerId = 2)
    {
       /*if ($workerId < 0 || $workerId > self::WORKER_MAX) {
           throw new Exception("Worker ID 超出范围");
        }

        $this->timestamp  = 0;
        $this->workerId   = $workerId;
        $this->sequence   = 0;*/
    }

    /**
     * 分配用户ID
     */
    public function generatedUniqueId(): int
    {
        try {
            return $this->getUniqueUserId();
        } catch (\Throwable $exception) {
            $this->logger->error('getUserSnowIdError', [
                'traceid'         => Context::get('traceid', ''),
                'client_tranceid' => Context::get('client_traceid', ''),
                'errorMsg'        => $exception->getMessage(),
                'site'            => $exception->getFile().$exception->getLine()
            ]);

            // dinDin alert
            NoticeFactory::dinDin(['webHookUrl' => self::WEB_HOOK_URL])->text(
                '[报警-用户中心]分配用户ID失败:'. $exception->getMessage().'；traceid=>' . Context::get('traceid', ''),
                [15869020161]
            );

            throw new \Exception('获取用户ID失败');
        }
    }

    /**
     * 获取用户ID
     *
     * @return int
     * @throws NotFoundDataException
     * @throws \Throwable
     */
    private function getUniqueUserId(): int
    {
        $redis = make(RedisFactory::class)->get('default');
        $id = $redis->sPop(self::USER_ID_KEY);

        if ($id !== false) {
           return (int) $id;
        }

        $rawIds = $this->getUniqueIds();
        $redis->pipeline();
        foreach ($rawIds as $k => $v) {
            $redis->sAdd(self::USER_ID_KEY, $v->user_id);
        }
        $redis->exec();

        // update user_id state
        go(function () use($rawIds) {
            $this->updateStateByUserIds($rawIds);
        });

        $id = $redis->sPop(self::USER_ID_KEY);
        return (int) $id;
    }

    /**
     * @return array
     * @throws NotFoundDataException
     * @throws \Throwable
     */
    private function getUniqueIds(): array
    {
        $ids = DB::connection('wzMain')->select('select user_id from user_ids_assign where state = 0 limit 10000');
        if (empty($ids)) {
            throw new CommonException(ErrorCode::SERVER_ERROR, '用户ID获取失败');
        }  

        return $ids;
    }

    /**
     * 通过用户ID更新 state 值
     * @param $ids
     */
    private function updateStateByUserIds($ids)
    {
        $_ids = [];
        $count = 0;

        $params = [];
        $db = DB::connection('wzMain');

        try {
            foreach ($ids as $val) {
                $_ids[] = $val->user_id;
                $count++;

                if ($count % 100 == 0) {
                    $sIds = implode(',', $_ids);
                    $sql = "update user_ids_assign set state = 1 where user_id in({$sIds})";
                    $upSuccessCount = $db->update($sql);

                    $params[] = [
                        'execSql' => $sql,
                        'updateSuccessCount'  => $upSuccessCount,
                    ];

                    $_ids = [];
                }
            }

            $this->logger->info('updateStateResByUserIds', [
                'updateParams' => json_encode($params, JSON_UNESCAPED_UNICODE),
                'companyId' => Context::get('company_id', 0),
            ]);
            $params = [];
        } catch (\Throwable $exception) {
            $secondUpdateRes = [];
            $secondUpdateCount = 0;
            foreach ($params as $sqlValue) {
                if (isset($sqlValue['execSql']) && !empty($sqlValue['execSql'])) {
                    $secondUpdateCount = $db->update($sqlValue['execSql']);
                    $secondUpdateRes[] = [
                        'secondExecSql' => $sqlValue['execSql'],
                        'secondUpCount' => $secondUpdateCount,
                    ];
                }
            }

            $this->logger->info('updateUserIdStateForError', [
                'traceid'         => Context::get('traceid', ''),
                'client_tranceid' => Context::get('client_traceid', ''),
                'params'          => $params ?? [],
                'secondUpdateRes' => json_encode($secondUpdateRes, JSON_UNESCAPED_UNICODE),
                'errorMsg'        => $exception->getMessage(),
                'site'            => $exception->getFile() .'|'. $exception->getLine()
            ]);

            NoticeFactory::dinDin(['webHookUrl' => self::WEB_HOOK_URL])->text(
                '[报警-用户中心]更新分配的用户ID状态失败：' .
                $exception->getMessage(). '；traceid => ' . Context::get('traceid', ''),
                [15869020161]
            );
        }

        $db = null;
    }

    /**
     * 获取当前毫秒
     * @return string
     */
    private function now(): string
    {
        return sprintf("%.0f", microtime(true) * 1000);
    }

    /**
     * 毫秒转秒
     *
     * @param $msValue
     * @return string
     */
    private function getSeconds($msValue): string
    {
        return sprintf("%.0f", $msValue / 1000);
    }

    /**
     * 生成ID
     *
     * @return int
     * @throws ClockBackwardException
     */
    private function old_generatedUniqueId(): int
    {
        $now = $this->now();

        try {
            //时间回拨判断
            if ($now < $this->timestamp) {
                $currentTime = date("Y-m-d H:i:s",  (int)$this->getSeconds($now));
                throw new CommonException(ErrorCode::SERVER_ERROR, '发生了时间回拨，生成ID失败；当前时间是：'. $currentTime);
            }

            if ($this->timestamp == $now) {
                $this->sequence = ($this->sequence+1) & self::SEQUENCE_BITS;

                //如果为0说明序号值达到了最大值，从下一毫秒重新计算
                if ($this->sequence == 0) {
                    while ($now <= $this->timestamp) {
                        $now = $this->now();
                    }
                }
            } else {
                $this->sequence = 0; //重新开始计算
            }

            $this->timestamp = $now;

            //获取ID值
            $id = (($now - self::EPOCH) << self::TIME_SHIFT) | ($this->workerId << self::WORKER_SHIFT) | $this->sequence;

            return $id;
        } catch (\Exception $e) {
            throw new CommonException($e->getCode(), '生成ID失败：'. $e->getMessage());
        }
    }

}
